﻿using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Media3D;
using System.Reflection;
using System.Linq;
using System.Threading.Tasks;

namespace SStuff.WpfEffects
{
    /// <summary>
    /// Shader Effect underlaying the PlaneProjection
    /// </summary>
    public class PlaneProjectionEffect : ShaderEffect
    {
        private PlaneProjectionTransform _hitTestingTransform = new PlaneProjectionTransform();

        /// <summary>
        /// When overridden in a derived class, transforms mouse input and coordinate systems through the effect. 
        /// </summary>
        /// <returns>
        /// The transform to apply. The default is the identity transform.
        /// </returns>
        protected override GeneralTransform EffectMapping
        {
            get
            {
                return _hitTestingTransform;
            }
        }

        /// <summary>
        /// Transform used only for Hit testing purpose
        /// </summary>
        private class PlaneProjectionTransform : GeneralTransform
        {
            public Size SourceSize { get; set; }

            private Matrix3D _planeTransformation;

            /// <summary>
            /// get or set Transform to the plane
            /// </summary>
            public Matrix3D PlaneTransformation
            {
                get { return _planeTransformation; }
                set { _planeTransformation = value; }
            }
            private Matrix3D _planeTransformI;

            /// <summary>
            /// get or set Inverse of the PlaneTransformation
            /// </summary>
            public Matrix3D PlaneTransformI
            {
                get { return _planeTransformI; }
                set { _planeTransformI = value; }
            }

            private Vector3D _planeNormal;

            /// <summary>
            /// Get or set the normal of the plane
            /// </summary>
            public Vector3D PlaneNormal
            {
                get { return _planeNormal; }
                set { _planeNormal = value; }
            }

            /// <summary>
            /// Get or set the offset applied to the ViewPort
            /// </summary>
            public Point ViewPortOffset { get; set; }


            private double _planeDistance;

            /// <summary>
            /// Get or set the distance to the plane
            /// </summary>
            public double PlaneDistance
            {
                get { return _planeDistance; }
                set { _planeDistance = value; }
            }



            /// <summary>
            /// Hack (returns itself)
            /// </summary>
            public override GeneralTransform Inverse
            {
                //This is a horrible fake
                get { return this; }
            }

            /// <summary>
            /// Not implemented
            /// </summary>
            /// <param name="rect"></param>
            /// <returns></returns>
            public override Rect TransformBounds(Rect rect)
            {
                // not used by hit testing
                // but used by blend for bounds transformation
                return rect;
            }

            /// <summary>
            /// When overridden in a derived class, attempts to transform the specified point and returns a value that indicates whether the transformation was successful.
            /// </summary>
            /// <returns>
            /// true if <paramref name="inPoint"/> was transformed; otherwise, false.
            /// </returns>
            /// <param name="inPoint">The point to transform.</param><param name="result">The result of transforming <paramref name="inPoint"/>.</param>
            public override bool TryTransform(Point inPoint, out Point result)
            {
                Vector3D inPoint3D = new Vector3D((inPoint.X * 2.0) - 1.0 + ViewPortOffset.X, (inPoint.Y * 2.0) - 1.0 + ViewPortOffset.Y, 0);

                Vector3D v = new Vector3D(0, 0, -1);


                double nv = Vector3D.DotProduct(_planeNormal, v);
                double ns = Vector3D.DotProduct(_planeNormal, inPoint3D);

                double t = -(ns + _planeDistance) / nv;

                // intersection point
                Vector3D intersectV = inPoint3D + t * v;
                Point3D intersect = new Point3D(intersectV.X, intersectV.Y, intersectV.Z);
                Point3D res3D = _planeTransformI.Transform(intersect);
                res3D.X /= SourceSize.Width*.5;
                res3D.Y /= SourceSize.Height * .5;
                result = new Point(.5 * res3D.X + .5, .5 * res3D.Y + .5);
                return result.X >= 0 && result.X <= 1 && result.Y >= 0 && result.Y <= 1;
            }

            /// <summary>
            /// When implemented in a derived class, creates a new instance of the <see cref="T:System.Windows.Freezable"/> derived class. 
            /// </summary>
            /// <returns>
            /// The new instance.
            /// </returns>
            protected override Freezable CreateInstanceCore()
            {
                return new PlaneProjectionTransform();
            }
        }

        #region Constructors

        /// <summary>
        /// Access to the GetViewMatrix internal method for the construction of the ViewProj matrix
        /// </summary>
        static MethodInfo _getViewMatrix = typeof(ProjectionCamera).GetMethod("GetViewMatrix", BindingFlags.Instance | BindingFlags.NonPublic);

        /// <summary>
        /// Access to the GetProjectionMatrix internal method for the construction of the ViewProj matrix
        /// </summary>
        static MethodInfo _getProjMatrix = typeof(PerspectiveCamera).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name == "GetProjectionMatrix" && m.GetParameters().Length == 1).First();

        /// <summary>
        /// PerspectiveCamera used to compute View / Proj matrices
        /// </summary>
        PerspectiveCamera _perspectiveCamera = new PerspectiveCamera(new Point3D(0, 0, 2.42), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45) { NearPlaneDistance = 0.2, FarPlaneDistance = 50 };
        Matrix3D _viewMatrix;

        Matrix3D _viewProjMatrix;

        public Matrix3D ViewProjMatrix { get { return _viewProjMatrix; } }


        Matrix3D _worldViewProj;
        Matrix3D _worldViewProjI;

        static PlaneProjectionEffect()
        {
            _pixelShader.UriSource = Global.MakePackUri("PlaneProjectionEffect.ps");
        }

        public PlaneProjectionEffect()
        {
            this.PaddingBottom = 1;
            this.PaddingTop = 1;
            this.PaddingLeft = 1;
            this.PaddingRight = 1;
            this.PixelShader = _pixelShader;
            CreateViewProjMatrix();

            SourceSize = new Size(100, 100);
            UpdateMatricesAndShaderValues();

            UpdateShaderValue(InputProperty);

            UpdateShaderValue(M1Property);
            UpdateShaderValue(M2Property);
            UpdateShaderValue(M3Property);
            UpdateShaderValue(M4Property);
            UpdateShaderValue(PlaneNormalProperty);
            UpdateShaderValue(PlaneDistanceProperty);
            UpdateShaderValue(ViewPortOffsetProperty);
           
        }



        #endregion

        #region Dependency Properties
        public Brush Input
        {
            get { return (Brush)GetValue(InputProperty); }
            set { SetValue(InputProperty, value); }
        }

        // Brush-valued properties turn into sampler-property in the shader.
        // This helper sets "ImplicitInput" as the default, meaning the default
        // sampler is whatever the rendering of the element it's being applied to is.
        public static readonly DependencyProperty InputProperty =
            ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(PlaneProjectionEffect), 0,SamplingMode.Bilinear);



        /// <summary>
        /// get or set World transformation of the plane
        /// </summary>
        public Matrix3D PlaneTransformation
        {
            get { return (Matrix3D)GetValue(PlaneTransformationProperty); }
            set { SetValue(PlaneTransformationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PlaneTransformation.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PlaneTransformationProperty =
            DependencyProperty.Register("PlaneTransformation", typeof(Matrix3D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(Matrix3D.Identity, (s, a) =>
            {
                (s as PlaneProjectionEffect).UpdateMatricesAndShaderValues();
            }));


        /// <summary>
        /// get or set a value indicating if the Viewport must be recentred to the transformed plane
        /// </summary>
        public bool RecenterViewPort
        {
            get { return (bool)GetValue(RecenterViewPortProperty); }
            set { SetValue(RecenterViewPortProperty, value); }
        }

        // Using a DependencyProperty as the backing store for RecenterViewPort.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RecenterViewPortProperty =
            DependencyProperty.Register("RecenterViewPort", typeof(bool), typeof(PlaneProjectionEffect), new UIPropertyMetadata(false));


        /// <summary>
        /// Get or set the viewport offset. If RecenterViewport is true, this value is computed by the shader itself
        /// </summary>
        public Point ViewPortOffset
        {
            get { return (Point)GetValue(ViewPortOffsetProperty); }
            set { SetValue(ViewPortOffsetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ViewPortOffset.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ViewPortOffsetProperty =
            DependencyProperty.Register("ViewPortOffset", typeof(Point), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Point(0, 0), PixelShaderConstantCallback(10)));

        static Point3D s_topLeft = new Point3D(-1, 1, 0);
        static Point3D s_topRight = new Point3D(1, 1, 0);
        static Point3D s_bottomLeft = new Point3D(-1, -1, 0);


        /// <summary>
        /// Update the matrices and plane equation and send it to the Pixel Shader
        /// </summary>
        private void UpdateMatricesAndShaderValues()
        {
            // Creating the worldViewProj matrix corresponding to the current plane transformation
            _worldViewProj = PlaneTransformation * _viewProjMatrix;
            if (RecenterViewPort)
            {
                Point3D transformedCenter = _worldViewProj.Transform(new Point3D(0, 0, 0));
                ViewPortOffset = new Point(transformedCenter.X, transformedCenter.Y);
                _hitTestingTransform.ViewPortOffset = ViewPortOffset;
            }
            _worldViewProjI = _worldViewProj;
            // and its inversed matrix
            _worldViewProjI.Invert();
            _hitTestingTransform.PlaneTransformation = _worldViewProj;
            _hitTestingTransform.PlaneTransformI = _worldViewProjI;


            // Calculate the 3 coordinates of the plane after transformation
            var rawTopLeft = Task.Factory.StartNew<Point3D>(() => _worldViewProj.Transform(s_topLeft));
            var rawTopRight = Task.Factory.StartNew<Point3D>(() => _worldViewProj.Transform(s_topRight));
            var rawBottomLeft = Task.Factory.StartNew<Point3D>(() => _worldViewProj.Transform(s_bottomLeft));

            // compute the normal of the plane
            var planeNormal = Vector3D.CrossProduct((rawTopRight.Result - rawTopLeft.Result), (rawBottomLeft.Result - rawTopLeft.Result));
            planeNormal.Normalize();

            PlaneNormal = planeNormal;
            _hitTestingTransform.PlaneNormal = planeNormal;

            // and its distance from the origin
            this.PlaneDistance = -Vector3D.DotProduct(planeNormal, new Vector3D(rawTopLeft.Result.X, rawTopLeft.Result.Y, rawTopLeft.Result.Z));
            _hitTestingTransform.PlaneDistance = this.PlaneDistance;
            // Pass it to the pixel shader
            // todo : find how to pass it in a way that mul(pos, m) works
            M1 = new Point4D(
                _worldViewProjI.M11,
                _worldViewProjI.M12,
                _worldViewProjI.M13,
                _worldViewProjI.M14);

            M2 = new Point4D(
                _worldViewProjI.M21,
                _worldViewProjI.M22,
                _worldViewProjI.M23,
                _worldViewProjI.M24);

            M3 = new Point4D(
                _worldViewProjI.M31,
                _worldViewProjI.M32,
                _worldViewProjI.M33,
                _worldViewProjI.M34);

            M4 = new Point4D(
                _worldViewProjI.OffsetX,
                _worldViewProjI.OffsetY,
                _worldViewProjI.OffsetZ,
                _worldViewProjI.M44);

        }



        public Size SourceSize
        {
            get { return (Size)GetValue(SourceSizeProperty); }
            set { SetValue(SourceSizeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SourceSize.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceSizeProperty =
            DependencyProperty.Register("SourceSize", typeof(Size), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Size(0, 0), new PropertyChangedCallback(PixelShaderConstantCallback(6)+ OnSourceSizeChanged)));

        private static void OnSourceSizeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var effect = sender as PlaneProjectionEffect;
            effect._hitTestingTransform.SourceSize = effect.SourceSize;
            effect.CreateViewProjMatrix();
            effect.UpdateMatricesAndShaderValues();
        }

        /// <summary>
        /// Get or set the Field of view in degrees
        /// </summary>
        public double FieldOfView
        {
            get { return (double)GetValue(FieldOfViewProperty); }
            set { SetValue(FieldOfViewProperty, value); }
        }

        // Using a DependencyProperty as the backing store for FieldOfView.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FieldOfViewProperty =
            DependencyProperty.Register("FieldOfView", typeof(double), typeof(PlaneProjectionEffect), new UIPropertyMetadata(45.0, (s, a) =>
            {
                var effect = s as PlaneProjectionEffect;
                effect.CreateViewProjMatrix();
                effect.UpdateMatricesAndShaderValues();
            }));

        private void CreateViewProjMatrix()
        {
            _perspectiveCamera.FieldOfView = FieldOfView;
            _perspectiveCamera.Position = new Point3D(0, 0, SourceSize.Width * 0.5 / (Math.Tan(Math.PI * FieldOfView / 360.0)));
            _viewMatrix = (Matrix3D)_getViewMatrix.Invoke(_perspectiveCamera, new object[] { });
            _viewProjMatrix = _viewMatrix * (Matrix3D)_getProjMatrix.Invoke(_perspectiveCamera, new object[] { SourceSize.IsEmpty ? 1.0 : SourceSize.Width / SourceSize.Height });
        }


        /// <summary>
        /// First row of the matrix sent to the shader
        /// </summary>
        public Point4D M1
        {
            get { return (Point4D)GetValue(M1Property); }
            set { SetValue(M1Property, value); }
        }

        // Using a DependencyProperty as the backing store for M1.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty M1Property =
            DependencyProperty.Register("M1", typeof(Point4D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Point4D(), PixelShaderConstantCallback(0)));



        /// <summary>
        /// Second row of the matrix sent to the shader
        /// </summary>
        public Point4D M2
        {
            get { return (Point4D)GetValue(M2Property); }
            set { SetValue(M2Property, value); }
        }

        // Using a DependencyProperty as the backing store for M2.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty M2Property =
            DependencyProperty.Register("M2", typeof(Point4D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Point4D(), PixelShaderConstantCallback(1)));



        /// <summary>
        /// Third row of the matrix sent to the shader
        /// </summary>
        public Point4D M3
        {
            get { return (Point4D)GetValue(M3Property); }
            set { SetValue(M3Property, value); }
        }

        // Using a DependencyProperty as the backing store for M3.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty M3Property =
            DependencyProperty.Register("M3", typeof(Point4D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Point4D(), PixelShaderConstantCallback(2)));



        /// <summary>
        /// Fourth row of the matrix sent to the shader
        /// </summary>
        public Point4D M4
        {
            get { return (Point4D)GetValue(M4Property); }
            set { SetValue(M4Property, value); }
        }

        // Using a DependencyProperty as the backing store for M4.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty M4Property =
            DependencyProperty.Register("M4", typeof(Point4D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Point4D(), PixelShaderConstantCallback(3)));



        /// <summary>
        /// Normal of the plane sent to the shader
        /// </summary>
        public Vector3D PlaneNormal
        {
            get { return (Vector3D)GetValue(PlaneNormalProperty); }
            set { SetValue(PlaneNormalProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PlaneNormal.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PlaneNormalProperty =
            DependencyProperty.Register("PlaneNormal", typeof(Vector3D), typeof(PlaneProjectionEffect), new UIPropertyMetadata(new Vector3D(), PixelShaderConstantCallback(4)));


        /// <summary>
        /// Plane distance sent to the shader
        /// </summary>
        public double PlaneDistance
        {
            get { return (double)GetValue(PlaneDistanceProperty); }
            set { SetValue(PlaneDistanceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PlaneDistance.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PlaneDistanceProperty =
            DependencyProperty.Register("PlaneDistance", typeof(double), typeof(PlaneProjectionEffect), new UIPropertyMetadata(0.0, PixelShaderConstantCallback(5)));








        #endregion



        #region Member Data

        private static PixelShader _pixelShader = new PixelShader();

        #endregion

    }
}
